# coding=utf-8
"""
作者：董新强 
创建时间：2020-5-15
描述：参数评估(主要是a,b的评估)
    步骤:
        1   给 theta和a初始值,求b
        2   通过theta和b,求a
        3   通过a,b求theta
        4   通过theta,a求b
        5   跳转至 2 继续
"""
import time
from datetime import datetime

import numpy as np
from scipy.optimize import differential_evolution

from db_models.cat_questions import CatQuestions
from db_models.param_estimate import ParamEstimate
from db_models.param_estimate_details import ParamEstimateDetails
from db_models.param_eval_answers import ParamEvalAnswers
from db_models.param_eval_test import ParamEvalTest
from framework.db_session import DbSession
from services.params_estimation import negative_log_likelihood_ab, negative_log_likelihood_theta


class ParamsEstimator:
    def __init__(self, aid, qnum_part):
        self.aid, self.qnum_part = aid, qnum_part
        self.matrix, self.col_to_qid, self.qids = ParamsEstimator._load_data(self.aid, self.qnum_part)
        self.person_nums, self.qnums = self.matrix.shape

        # 共享参数
        # 初始化能力值(这个阶段,需要的是计算出a和b,用户的得分值就不管具体是谁得分了)
        self.theta_arr = np.array([0.0 for _ in range(self.person_nums)], dtype=np.float)
        self.a_arr = np.array([1.0 for _ in range(self.qnums)], dtype=np.float)
        self.b_arr = np.array([0.0 for _ in range(self.qnums)], dtype=np.float)
        self._finished = False

    @classmethod
    def _load_data(cls, aid, qnum_part):
        """
        载入数据,将数据组织为一个矩阵,其中,
            行 为一个人测试答案
            列 为一题选择的答案
        """
        responses, qids = {}, set()  # 答案,题目列表
        with DbSession.create() as db:
            query = db.query(ParamEvalAnswers.test_id, ParamEvalAnswers.qid, ParamEvalAnswers.answer) \
                .filter(ParamEvalTest.aid == aid) \
                .filter(ParamEvalTest.qnum_part == qnum_part) \
                .filter(ParamEvalAnswers.test_id == ParamEvalTest.id) \
                .filter(ParamEvalTest.status == True) \
                .order_by(ParamEvalAnswers.test_id) \
                .order_by(ParamEvalAnswers.qid)
            for test_id, qid, answer in query:
                responses[(test_id, qid)] = answer  # 去重
                qids.add(qid)

        # 构造题号到列的映射
        qids = list(qids)
        qid_to_col, qnums = {}, len(qids)
        for idx, qid in enumerate(qids):
            qid_to_col[qid] = idx

        # 构造矩阵
        user_answers_dict = {}
        for (test_id, qid), answer in responses.items():
            if test_id in user_answers_dict:
                user_answers_dict[test_id][qid_to_col[qid]] = answer
            else:
                user_answers_dict[test_id] = [False for _ in range(qnums)]
                user_answers_dict[test_id][qid_to_col[qid]] = answer

        matrix = np.array([v for v in user_answers_dict.values()], dtype=np.bool)
        col_to_qid = {v: k for k, v in qid_to_col.items()}
        return matrix, col_to_qid, qids

    def estimate(self, epolls=5, is_print=False):
        """
        评估
        """
        for epoll in range(epolls):

            # 评估 难度和区分度
            st = time.time()
            for col in range(self.qnums):
                self.a_arr[col], self.b_arr[col] = differential_evolution(
                    negative_log_likelihood_ab, bounds=[[0, 1], [-3, 3]], args=(self.theta_arr, 0.25, 1.0, self.matrix[:, col])
                ).x
            ed_ab = time.time() - st

            # 评估 能力
            st = time.time()
            for row in range(self.person_nums):
                self.theta_arr[row] = differential_evolution(
                    negative_log_likelihood_theta, bounds=[[-3, 3]], args=(self.a_arr, self.b_arr, 0.25, 1.0, self.matrix[row, :])
                ).x[0]
            ed_theta = time.time() - st

            if is_print:
                print('第%d次循环' % (epoll,), '\ta b 评估耗时=>%.3f' % ed_ab, '\t\ttheta评估耗时=>%.3f' % ed_theta, '\t\ta b 单次耗时=>', '%.3f' % (ed_ab / self.qnums), '\t\ttheta单次耗时=>', '%.3f' % (ed_theta / self.person_nums))

            # ParamsEstimator.echo('区分度', self.a_arr)
            # ParamsEstimator.echo('难度', self.b_arr)
            # ParamsEstimator.echo('能力', self.theta_arr)

        self._finished = True
        return self

    def format(self):
        for idx, (a, b) in enumerate(zip(self.a_arr, self.b_arr)):
            print('题目:%d' % (self.col_to_qid[idx],), '\t区分度:%.2f' % (a,), '\t难度:%.2f' % (b,))
        return self

    @staticmethod
    def echo(title, arr):
        print(title, '=>  ', end='')
        for v in arr:
            print('%.2f' % (v), end='  ')
        print()

    def save_paramters_to_db(self):
        '''
        将评估参数保存到数据库
        :return:
        '''
        if not self._finished:
            print('评估未结束,无法更新数据库')

        detail_ls = []
        for idx, (a, b) in enumerate(zip(self.a_arr, self.b_arr)):
            qid = self.col_to_qid[idx]
            detail = ParamEstimateDetails()
            detail.qid = qid
            detail.b_new = float(b)
            detail.a_new = float(a)
            detail_ls.append(detail)

        with DbSession.create() as db:
            main = ParamEstimate()
            main.aid = self.aid
            main.create_time = datetime.now()
            main.status = 0
            db.add(main)
            db.flush()
            pid = main.id

            old_ab_dic = {}
            query = db.query(CatQuestions.id, CatQuestions.a, CatQuestions.b).filter(CatQuestions.id.in_(self.qids))
            for qid, a, b in query:
                old_ab_dic[qid] = (a, b)

            for detail in detail_ls:
                a_old, b_old = old_ab_dic[detail.qid]
                detail.a_old = a_old
                detail.b_old = b_old
                detail.pid = pid

            db.bulk_save_objects(detail_ls)
        print('更新参数成功,等待服务更新参数')
        self._finished = False


if __name__ == '__main__':
    estimator = ParamsEstimator(aid=1, qnum_part=0)
    estimator.estimate(epolls=20, is_print=True).format().save_paramters_to_db()
